package pci.recommend

import java.io._
import scala.Math._
import scala.collection.mutable.Map

/**
 * zhu.tan@gmail.com
 * 17-Jun-2009
 */

object Recommend {
  type PrefsType = Map[String, Map[String, Double]];

  private def sum(initial: Double, xs: Iterable[Double]): Double = {
    (initial /: xs)(_ + _)
  }

  //   Returns a distance-based similarity score for person1 and person2
  def sim_distance_euclidean(prefs: PrefsType, p1: String, p2: String): Double = {
    // Get the list of shared_items
    val sharedItem = for (item1 <- prefs(p1); item2 <- prefs(p2) if item1._1 == item2._1)
    yield item1._1
    if (sharedItem.isEmpty) return 0

    var sum_of_squares: Double = 0.0
    sharedItem.foreach(i => sum_of_squares += Math.pow(prefs(p1)(i) - prefs(p2)(i), 2))
    // alternative implementation
    // val sum = (0.0 /:  sharedItem.map(i => Math.pow(prefs(p1)(i) - prefs(p2)(i), 2)))(_ + _)

    return 1 / (1 + sum_of_squares)
  }

  // Returns the Pearson correlation coefficient for p1 and p2
  def sim_pearson(prefs: PrefsType, p1: String, p2: String): Double = {
    val sharedItem = for (item1 <- prefs(p1); item2 <- prefs(p2) if item1._1 == item2._1)
    yield item1._1

    // if they are not rating in common, return 0
    if (sharedItem.isEmpty) return 0

    // find the number of element
    val n = sharedItem.toList.length

    // sums of all the perferences & their squares
    val sum1 = sum(0.0, for (item <- sharedItem; pref <- prefs(p1) if pref._1 == item) yield pref._2)
    val sum2 = sum(0.0, sharedItem.map(prefs(p2)(_)))

    val sum1Sq = sum(0.0, sharedItem.map(x => pow(prefs(p1)(x), 2.0)))
    val sum2Sq = sum(0.0, sharedItem.map(x => pow(prefs(p2)(x), 2.0)))

    // Sum of the products
    val pSum = sum(0.0, sharedItem.map(x => prefs(p1)(x) * prefs(p2)(x)))

    //Calculate r (Pearson score)
    val num = pSum - (sum1 * sum2 / n)
    val den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n))
    if (den == 0) return 0

    num / den
  }

  // returns thebest matches for person from the perfs dictionary
  def topMatches(prefs: PrefsType, p: String, sim_calc: (PrefsType, String, String) => Double)
  : List[(Double, String)] = {
    val others = prefs.keys.filter(_ != p)
    val result = for (ano <- others) yield (sim_calc(prefs, ano, p), ano)
    result.toList.sort((x, y) => x._1 > y._1) // from high to low score.
  }

  def getRecommendations(prefs: PrefsType, p: String,
                         sim_calc: (PrefsType, String, String) => Double)
  : List[(Double, String)] = {
    val totals = Map[String, Double]()
    val simSums = Map[String, Double]()

    val others = prefs.keys.filter(_ != p)
    for (other <- others) {
      val sim = sim_calc(prefs, p, other)
      if (sim > 0) {
        for (item <- prefs(other) if !prefs(p).contains(item._1)) {
          // Similarity * Score
          totals.getOrElseUpdate(item._1, 0.0)
          totals(item._1) = totals(item._1) + prefs(other)(item._1) * sim

          simSums.getOrElseUpdate(item._1, 0.0)
          simSums(item._1) = simSums(item._1) + sim
        }
      }
    }

    val re = for ((item, total) <- totals) yield (total / simSums(item), item)
    re.toList.sort((x, y) => x._1 > y._1) // from high to low score.
  }

  def transformPrefs(prefs: PrefsType) = {
    val result = Map[String, Map[String, Double]]()
    for (person <- prefs.keys; item <- prefs(person)) {
      if (result.contains(item._1))
        result(item._1) = result(item._1) + (person -> item._2)
      else
        result += (item._1 -> Map(person -> item._2))
    }
    result
  }

  def calculateSimilarItems(prefs: PrefsType) = {
    //    Invert the preference matrix to be item-centric
    val result = Map[String, List[(Double, String)]]()
    val itemPrefs = transformPrefs(prefs)
    var c = 0L
    val size = itemPrefs.size
    for (item <- itemPrefs.keys) {
      c += 1;
      if (c % 100 == 0) println(c + "/" + size);
      val score = topMatches(itemPrefs, item, sim_distance_euclidean)
      result += (item -> score)
    }
    result
  }

  def getRecommendedItems(prefs: PrefsType, itemMatch: Map[String, List[(Double, String)]], user: String) = {
    val userRatings = prefs(user)
    val scores = Map[String, Double]() //   {  override def default(key : String) = 0.0}
    val totalSim = Map[String, Double]()

    // Loop over items rated by this user
    for ((item, rating) <- userRatings) {
      // Loop over items similar to this one, Ignore if this user has already rated this item
      for ((similarity, item2) <- itemMatch(item) if !(userRatings contains item2)) {
        // Weighted sum of rating times similarity                                           
        scores.getOrElseUpdate(item2, 0.0)
        scores(item2) += similarity * rating
        // Sum of all the similarities
        totalSim.getOrElseUpdate(item2, 0.0)
        totalSim(item2) += similarity
      }
    }
    // Divide each total score by total weighting to get an average
    val rankings = for ((item, score) <- scores) yield (score / totalSim(item), item)
    rankings.toList.sort((x, y) => x._1 > y._1) // from high to low score.
  }


  def loadMovieLens(path: String) = {
    val movies = Map[String, String]()
    /*
         For 2.8:
         Source.fromPath( path + "u.item")("ISO-8859-1").getLines.foreach{
             ...
         }

     */
    val ms = scala.io.Source.fromFile(new File(path, "u.item"), "ISO-8859-1").getLines


    ms.foreach(line => {
      val idName = line.split('|').take(2)
      movies(idName.first) = idName.last
    })
    
    val prefs = Map[String, Map[String, Double]]()
    val lines = scala.io.Source.fromFile(new File(path, "u.data"), "ISO-8859-1").getLines
    for(line <- lines) {
      val ds = line.split('\t')
      prefs.getOrElseUpdate(ds(0),Map[String, Double]())
      prefs(ds(0)) += {movies(ds(1)) -> ds(2).toDouble}
    }
    prefs
  }
}